package com.google.code.multitester.annonations.processors;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * Placeholder for invoking methods. This class helps keep method and enclosing
 * instance together. Also there are some verifications and exceptions handling.
 * <p>
 * Only so called 'void-method' can be invoked. 'void-method' is a method that
 * doesn't take any arguments and doesn't return any value. Also 'void-method'
 * should not be static.
 *
 * @author Yuriy Sechko
 */
public class VoidMethodPointer
{
	private final Object instance;
	private final Method method;

	/**
	 * Creates placeholder and connects method and enclosing instance.
	 * Placeholder cannot be created in some circumstances.
	 * 
	 * @param instance
	 *            object on which method will be called
	 * @param method
	 *            method that will be called
	 * 
	 * @throws java.lang.IllegalArgumentException
	 *             when:
	 *             <ul>
	 *             <li>given instance or method is null</li>
	 *             <li>given method is static</li>
	 *             <li>given method returns something that differs from
	 *             {@code void} or {@code Void}</li>
	 *             <li>given method takes one or more arguments</li>
	 *             <li>given method doesn't belong to the specified class or to
	 *             the one of their superclasses</li>
	 *             </ul>
	 */
	public VoidMethodPointer(final Object instance, final Method method)
	{
		Checkings.instanceIsNull(instance);
		Checkings.methodIsNull(method);
		Checkings.methodIsStatic(method);
		Checkings.isReturnTypeAcceptable(method);
		Checkings.areThereSomeArguments(method);
		Checkings.areMethodAndInstanceTypeInTheSameHierarchy(instance, method);
		this.instance = instance;
		this.method = method;
	}

	/**
	 * Nothing special. Equality is calculated by equality of the
	 * {@link java.lang.Object} and equality of the
	 * {@link java.lang.reflect.Method}.
	 */
	@Override
	public boolean equals(Object o)
	{
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;

		VoidMethodPointer pointer = (VoidMethodPointer) o;

		if (instance != null ? !instance.equals(pointer.instance)
				: pointer.instance != null)
			return false;
		if (method != null ? !method.equals(pointer.method)
				: pointer.method != null)
			return false;

		return true;
	}

	/**
	 * Nothing special. Code for calculating hash is provided by IDE.
	 */
	@Override
	public int hashCode()
	{
		int result = instance != null ? instance.hashCode() : 0;
		result = 31 * result + (method != null ? method.hashCode() : 0);
		return result;
	}

	/**
	 * Invokes method on the specified instance. Method's access modifier
	 * doesn't matter.
	 * 
	 * @throws java.lang.IllegalAccessException
	 *             when method cannot be accessed
	 * 
	 * @throws java.lang.reflect.InvocationTargetException
	 *             when something goes wrong inside method that invoked
	 */
	public void invoke()
	{
		try
		{
			method.setAccessible(true);
			method.invoke(instance);
			method.setAccessible(false);
		}
		catch (IllegalAccessException accessException)
		{
			throw new IllegalStateException(composeErrorMessage(instance,
					method), accessException);
		}
		catch (InvocationTargetException invocationException)
		{
			throw new IllegalStateException(composeErrorMessage(instance,
					method), invocationException);
		}
	}

	private static String composeErrorMessage(final Object instance,
			final Method method)
	{
		return String.format(
				"Cannot invoke method %s on %s instance via reflection.",
				method.getName(), instance.getClass().getCanonicalName());
	}

	private static class Checkings
	{
		// Only 'void' and 'Void' return type are accepted
		private static final Set<Class> acceptableReturnTypes;
		static
		{
			final Set<Class> tempSet = new HashSet<Class>();
			tempSet.add(Void.TYPE);
			tempSet.add(Void.class);
			acceptableReturnTypes = Collections.unmodifiableSet(tempSet);
		}

		private static void methodIsStatic(final Method method)
		{
			if ((method.getModifiers() & Modifier.STATIC) != 0)
			{
				throw new IllegalArgumentException(
						"Method should not be static.");
			}
		}

		private static void methodIsNull(final Method method)
		{
			if (method == null)
			{
				throw new IllegalArgumentException(
						"Method placeholder should not be null.");
			}
		}

		private static void instanceIsNull(final Object instance)
		{
			if (instance == null)
			{
				throw new IllegalArgumentException(
						"Instance for calling method should not be null.");
			}
		}

		private static void isReturnTypeAcceptable(final Method method)
		{
			final Class methodReturnType = method.getReturnType();
			if (!acceptableReturnTypes.contains(methodReturnType))
			{
				final String message = String
						.format("Method should return 'void' or 'Void', but returns '%s'",
								methodReturnType.getCanonicalName());
				throw new IllegalArgumentException(message);
			}
		}

		private static void areThereSomeArguments(Method method)
		{
			if (method.getParameterTypes().length != 0)
			{
				throw new IllegalArgumentException(
						"Method should not take any arguments.");
			}
		}

		private static void areMethodAndInstanceTypeInTheSameHierarchy(
				final Object instance, final Method method)
		{
			if (!method.getDeclaringClass().isAssignableFrom(
					instance.getClass()))
			{
				throw new IllegalArgumentException(
						"Method's declared class should be assignable from instance class.");
			}
		}
	}
}
